package bsserver.response;

import bsserver.Auth;
import bsserver.Main;
import bsserver.Request;
import bsserver.RequestURI;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.TimeZone;

/**
 * Generic Response object to handle HTTP Responses.
 * @author tblackstone
 */
public abstract class Response {

    /**
     * The HTTP version to use in this Response
     */
    protected String version;
    /**
     * The status code to return in this Response
     */
    protected String statusCode;
    /**
     * The reason phrase to return in the Response
     */
    protected String reasonPhrase;
    private HashMap<String, String> headers;
    private byte[] messageBody;
    /**
     * A static list of standard response codes and their default reason phrases.
     * Taken from RFC 2616, section 6.1.1
     */
    public final static HashMap<Integer, String> responseCodes =
            new HashMap<Integer, String>();

    /**
     * Generate a Response to the given Request.  Responses are generally
     * handled by the class which corresponds to the client's Request method,
     * but may be generated by other means, e.g. in the case of an error or
     * CGI script execution.
     * @param r the incoming Request to respond to
     * @return the appropriately generated Response
     */
    public static Response generate(Request r) {

        Response response;
        if (r.getMethod().equals("GET")) {
            response = (Response) GETResponse.process(r);
        } else if (r.getMethod().equals("HEAD")) {
            response = (Response) HEADResponse.process(r);
        } else if (r.getMethod().equals("POST")) {
            response = (Response) POSTResponse.process(r);
        } else if (r.getMethod().equals("PUT")) {
            response = (Response) PUTResponse.process(r);
        } else if (r.getMethod().equals("DELETE")) {
            response = (Response) new ErrorResponse(r, 501);
        } else if (r.getMethod().equals("TRACE")) {
            response = (Response) new ErrorResponse(r, 501);
        } else if (r.getMethod().equals("CONNECT")) {
            response = (Response) new ErrorResponse(r, 501);
        } else {
            response = (Response) new ErrorResponse(r, 400);
        }

        return response;
    }

    /**
     * Default Response constructor.  All implementing subclasses should call
     * super(r) to properly initialize any required Respose parameters
     * @param r the incoming Request to respond to
     */
    protected Response(Request r) {

        headers = new HashMap<String, String>();
        if (r.getHTTPVersion() == null) {
            version = "HTTP/1.1";
        } else {
            version = r.getHTTPVersion();
        }

    }

    /**
     * Sets up default headers to include in all Response objects
     */
    protected void setDefaultHeaders() {
        // Set Date header
        SimpleDateFormat f = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
        f.setTimeZone(TimeZone.getTimeZone("GMT"));
        String date = f.format(new Date());
        setHeader("Date", date);

        // Set Server header
        setHeader("Server", Main.HttpdConfig.get("ServerName"));
    }

    /**
     * Sets the status code of the Response.
     * @param status the status code to use (per RFC 2616 section 6.1.1)
     */
    protected void setStatus(int status) {
        statusCode = Integer.toString(status);
        reasonPhrase = Response.responseCodes.get(status);
    }

    /**
     * Saves a response header field/value pair to be sent to the client.  Any
     * existing header with the same field name is replaced.
     * @param header the request header field to store
     * @param value the value of the response header field
     */
    protected void setHeader(String header, String value) {
        headers.put(header, value);
    }

    /**
     * Saves the body of the HTTP response.
     * @param body A String containing the entire message body.
     */
    protected void setBody(String body) {
        try {
            setBody(body.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException ex) {
            setBody(body.getBytes());
        }
    }

    /**
     * Saves the body of the HTTP response.
     * @param body A byte array containing the entire message body.
     */
    protected void setBody(byte[] body) {
        messageBody = body;
        setHeader("Content-length", Integer.toString(messageBody.length));
    }

    /**
     * Outputs the entire Response to a given output stream. This includes the
     * response status, any headers, and the message body.
     * @param out the stream to write the Response to
     * @return the number of bytes written to the OutputStream
     */
    public int printResponse(OutputStream out) {

        String outputString = version + " " + statusCode + " " + reasonPhrase + "\r\n";

        for (String header : headers.keySet()) {
            String headerLine = header + ": " + headers.get(header);
            outputString = outputString + headerLine + "\r\n";
        }

        byte[] messageHeaders;
        outputString = outputString + "\r\n";

        try {
            messageHeaders = outputString.getBytes("UTF-8");
        } catch (UnsupportedEncodingException ex) {
            messageHeaders = outputString.getBytes();
        }


        try {
            out.write(messageHeaders);
            if (messageBody != null) {
                // Only write message body if it exists
                out.write(messageBody);
            }
        } catch (IOException ex) {
            // Error writing response to client, maybe disconnected?
            // Just ignore it...
        }

        if (messageBody != null) {
            return (messageHeaders.length + messageBody.length);
        } else {
            return (messageHeaders.length);
        }
    }

    /**
     * Gets the HTTP status code that this Response will return.
     * @return the HTTP status code (as defined in RFC 2616)
     */
    public String getStatusCode() {
        return statusCode;
    }

    /**
     * Checks if the client has sufficient permission to access the requested
     * resource. This is done by first checking if the resource requires
     * authentication.  If so, the request is examined to see if authentication
     * was provided.  Finally, the provided authentication is validated per
     * server security access directives.
     * @param r The Request resource sent by the client.
     * @return the user name provided by the client if granted access to the resource,
     * otherwise null
     */
    protected static String findClientPermissions(Request r) {

        RequestURI requestURI = r.getRequestURI();

        // Check Permissions
        Auth permissions = new Auth(requestURI.getAbsolutePath());

        r.setAuthInfo(permissions);

        if (permissions.isPasswordProtected()) {
            String authResponse = r.getHeader("Authorization");
            // If user has provided an authorization name/password in header
            if (authResponse != null) {
                String[] authLine = authResponse.trim().split(" ");
                if (authLine.length != 2) {
                    return null;
                }


                // Handle Basic Authentication
                if (authLine[0].equals("Basic")) {
                    if (permissions.checkBase64UserPassword(authLine[1])) {
                        // User/password is valid
                        return permissions.getAuthUser();
                    }

                    // Wrong user/password!
                    return null;
                }

            } else {
                // User did not provide Authentication yet
                return null;
            }
        }

        // No authentication needed for the selected resource
        return "";

    }


    static {
        responseCodes.put(100, "Continue");
        responseCodes.put(101, "Switching Protocols");
        responseCodes.put(200, "OK");
        responseCodes.put(201, "Created");
        responseCodes.put(202, "Accepted");
        responseCodes.put(203, "Non-Authoritative Information");
        responseCodes.put(204, "No Content");
        responseCodes.put(205, "Reset Content");
        responseCodes.put(206, "Partial Content");
        responseCodes.put(300, "Multiple Choices");
        responseCodes.put(301, "Moved Permanently");
        responseCodes.put(302, "Found");
        responseCodes.put(303, "See Other");
        responseCodes.put(304, "Not Modified");
        responseCodes.put(305, "Use Proxy");
        responseCodes.put(307, "Temporary Redirect");
        responseCodes.put(400, "Bad Request");
        responseCodes.put(401, "Unauthorized");
        responseCodes.put(402, "Payment Required");
        responseCodes.put(403, "Forbidden");
        responseCodes.put(404, "Not Found");
        responseCodes.put(405, "Method Not Allowed");
        responseCodes.put(406, "Not Acceptable");
        responseCodes.put(407, "Proxy Authentication Required");
        responseCodes.put(408, "Request Time-out");
        responseCodes.put(409, "Conflict");
        responseCodes.put(410, "Gone");
        responseCodes.put(411, "Length Required");
        responseCodes.put(412, "Precondition Failed");
        responseCodes.put(413, "Request Entity Too Large");
        responseCodes.put(414, "Request-URI Too Large");
        responseCodes.put(415, "Unsupported Media Type");
        responseCodes.put(416, "Requested range not satisfiable");
        responseCodes.put(417, "Expectation Failed");
        responseCodes.put(500, "Internal Server Error");
        responseCodes.put(501, "Not Implemented");
        responseCodes.put(502, "Bad Gateway");
        responseCodes.put(503, "Service Unavailable");
        responseCodes.put(504, "Gateway Time-out");
        responseCodes.put(505, "HTTP Version not supported");
    }
}
